using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using WindowsPhoneEssentials.Abstractions;
using WindowsPhoneEssentials.ComponentModel;
using WindowsPhoneEssentials.Helpers;

namespace WindowsPhoneEssentials.Settings
{
    public class SettingsProvider : ISettingsProvider
    {
        private const string NotConvertableMessage = "Settings provider only supports types that Convert.ChangeType supports. See http://msdn.microsoft.com/en-us/library/dtb69x08.aspx";
        private readonly IIsolatedStorageSettings settingsRepository;

        public SettingsProvider(IIsolatedStorageSettings settingsRepository)
        {
            this.settingsRepository = settingsRepository;
        }

        public T GetSettings<T>() where T : ISettings, new()
        {
            return LoadSettings<T>();
        }

        private T LoadSettings<T>() where T : ISettings, new()
        {
            var settings = new T();
            var settingMetadata = ReadSettingMetadata<T>();

            foreach (var setting in settingMetadata)
            {
                // Initialize with default values
                setting.Write(settings, setting.DefaultValue);

                // Write over it using the stored value if exists
                var key = GetKey<T>(setting);
                if (settingsRepository.ContainsKey(key))
                {
                    setting.Write(settings, ConvertValue((string)settingsRepository[key], setting));
                }
            }

            return settings;
        }

        private static object ConvertValue(string storedValue, SettingDescriptor setting)
        {
            if (storedValue == null) return null;
            if (setting.UnderlyingType == typeof(string)) return storedValue;
            if (setting.UnderlyingType != typeof(string) && string.IsNullOrEmpty(storedValue)) return null;
            if (setting.UnderlyingType.IsEnum) return Enum.Parse(setting.UnderlyingType, storedValue, false);

            object converted;
            try
            {
                converted = Convert.ChangeType(storedValue, setting.UnderlyingType, CultureInfo.InvariantCulture);
            }
            catch (InvalidCastException ex)
            {
                throw new NotSupportedException(NotConvertableMessage, ex);
            }
            catch (FormatException ex)
            {
                throw new NotSupportedException(NotConvertableMessage, ex);
            }

            return converted;
        }

        public void SaveSettings<T>(T settingsToSave) where T : ISettings
        {
            var settingsMetadata = ReadSettingMetadata<T>();

            foreach (var setting in settingsMetadata)
            {
                var value = setting.ReadValue(settingsToSave) ?? setting.DefaultValue;
                if (value == null && setting.UnderlyingType.IsEnum)
                    value = EnumHelper.GetValues(setting.UnderlyingType).First();
                settingsRepository[GetKey<T>(setting)] = Convert.ToString(value ?? string.Empty, CultureInfo.InvariantCulture);
            }
        }

        internal static string GetKey<T>(SettingDescriptor setting)
        {
            var settingsType = typeof(T);

            return string.Format("{0}.{1}", settingsType.FullName, setting.Property.Name);
        }

        public static IEnumerable<SettingDescriptor> ReadSettingMetadata<T>() where T : ISettings
        {
            return ReadSettingMetadata(typeof(T));
        }

        public static IEnumerable<SettingDescriptor> ReadSettingMetadata(Type settingsType)
        {
            if (!typeof (ISettings).IsAssignableFrom(settingsType))
                throw new InvalidOperationException("settingsType must inherit ISettings");

            return settingsType.GetProperties()
                .Where(x => x.CanRead && x.CanWrite)
                .Select(x => new SettingDescriptor(x))
                .ToArray();
        }

        public class SettingDescriptor
        {
            private readonly PropertyInfo property;

            public SettingDescriptor(PropertyInfo property)
            {
                this.property = property;
                DisplayName = property.Name;

                ReadAttribute<DefaultValueAttribute>(d => DefaultValue = d.Value);
                ReadAttribute<DescriptionAttribute>(d => Description = d.Description);
                ReadAttribute<DisplayNameAttribute>(d => DisplayName = d.DisplayName);
            }

            private void ReadAttribute<TAttribute>(Action<TAttribute> callback)
            {
                var instances = property.GetCustomAttributes(typeof(TAttribute), true).OfType<TAttribute>();
                foreach (var instance in instances)
                {
                    callback(instance);
                }
            }

            public PropertyInfo Property
            {
                get { return property; }
            }

            public object DefaultValue { get; private set; }

            public string Description { get; private set; }

            public string DisplayName { get; private set; }

            public void Write(ISettings settings, object value)
            {
                property.SetValue(settings, value, null);
            }

            /// <summary>
            /// If the property type is nullable, returns the type. i.e int? returns int
            /// </summary>
            public Type UnderlyingType
            {
                get
                {
                    if (Property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                        return property.PropertyType.GetGenericArguments()[0];
                    return property.PropertyType;
                }
            }

            public object ReadValue(ISettings settings)
            {
                return property.GetValue(settings, null);
            }
        }
    }
}